﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NDesk.Options;
using System.IO;
using System.Reflection;
using cilada.engine;
using cilada.engine.Graph;
using System.Diagnostics;

namespace cilada
{
    class Program
    {
        private static string[] ASSEMBLY_EXTENSIONS = new string[] { ".exe", ".dll" };

        static void Main(string[] args)
        {
            RunProgram(args);
        }

        private static void RunProgram(string[] args)
        {
            #region Option Parsing
            bool show_help = false;
            bool pauseAtCompletion = false;
            string outputPath = String.Empty;
            string outputFileName = String.Empty;
            List<string> assemblyAnalysisPaths = new List<string>();
            List<string> dependencyDirs = new List<string>();
            bool recursive = false;
            bool addDependencySubDirs = true; 
            bool verbose = false;

            var p = new OptionSet() {
            { "o|outdir=", "the full path to a directory to put the output in. It will be created if it does not exist.",
              v => outputPath = v.Trim() },

            { "n|outname=", "specifies the name the output file will have. The extension .agraphml will be added if it is not already.",
              v => outputFileName = v.Trim() },

            { "p|pause", "the program will require a key press to close the command prompt once it has finished executing.",
              v => pauseAtCompletion = true },

            { "d|dir=", "a full directory path. Will add all the assemblies in it to the analysis.",
              v => assemblyAnalysisPaths.Add(v) },

            { "s|seachpath=", "a full directory path. Will be added to the search path for assemblies. Assemblies in these folders will not analysed.",
              v => dependencyDirs.Add(v) },

            { "notaddrefsubdirs", "any subdirectories of the given dependency dirs will not be added to the dependency search path.",
              v => addDependencySubDirs = false },

            { "r|recursive", "any analysis directories specified will be recursively searched for assemblies",
              v => recursive = true },

            { "v|verbose", "verbose console output.",
              v => verbose = true },

            { "h|help",  "show the help message and exit.",
              v => show_help = true },
            };

            try
            {
                List<string> singlePaths = p.Parse(args);
                assemblyAnalysisPaths.AddRange(singlePaths);
            }
            catch (OptionException oe)
            {
                // TODO - Logging
                Console.WriteLine("Invalid option or parameter on " + oe.OptionName);
                Console.WriteLine("Try 'cilada --help' for more information.");
                ShowHelp(p);
                return;
            }

            if (show_help)
            {
                ShowHelp(p);
                return;
            }

            if (outputPath == String.Empty)
            {
                System.Reflection.Assembly a = System.Reflection.Assembly.GetEntryAssembly();
                outputPath = a.Location;
                Console.WriteLine("Output dir is not set. Setting it to {0}", outputPath);
            }

            if (String.Empty.Equals(outputFileName))
            {
                outputFileName = String.Format("output-{0}.xml", DateTime.UtcNow.ToShortDateString()); ;
                Console.WriteLine("Output fileName is not set. Setting it to {0}", outputFileName);
            }
            else if (!outputFileName.ToLower().EndsWith(".xml"))
            {
                outputFileName += ".xml";
            }

            // verify all given dependency paths exist, and add all subdirectories.
            List<string> tempList = new List<string>();
            tempList.AddRange(dependencyDirs);
            foreach (string path in tempList)
            {
                if (!Directory.Exists(path))
                {
                    dependencyDirs.Remove(path);
                }
                else
                {
                    if (addDependencySubDirs)
                    {
                        List<string> subDirs = GetAllSubDirs(new DirectoryInfo(path));
                        dependencyDirs.AddRange(subDirs);
                    }
                }
            }

            // collect all the assemblies for analysis
            List<string> analysisAssemblies = ProcessPaths(assemblyAnalysisPaths, recursive);

            IDictionary<string, string> assemblyNamesAndPaths = new Dictionary<string, string>();
            if (analysisAssemblies.Count == 0)
            {
                Console.WriteLine("No assemblies specified!");
                Console.WriteLine();
                ShowHelp(p);
                return;
            }
            else
            {
                // remove any duplicates of assembly names, even if the paths are different.
                // TODO: work out how to handle the case of multiple assemblies with the same names
                // with different paths. Currently, it just ignores any names that have multiple paths.
                IDictionary<string, List<string>> pathSets = new Dictionary<string, List<string>>();
                string name = String.Empty;
                foreach (string path in analysisAssemblies)
                {
                    name = Path.GetFileName(path);
                    if (pathSets.ContainsKey(name))
                    {
                        pathSets[name].Add(path);
                    }
                    else
                    {
                        List<string> dupListPaths = new List<string>();
                        dupListPaths.Add(path);
                        pathSets.Add(name, dupListPaths);
                    }
                }

                foreach (KeyValuePair<string, List<string>> kvp in pathSets)
                {
                    if (kvp.Value.Count.Equals(1))
                    {
                        assemblyNamesAndPaths.Add(kvp.Key, kvp.Value[0]);
                    }
                    else
                    {
                        Console.WriteLine("# Duplicate assembly names found at different paths:");
                        Console.WriteLine(kvp.Key);
                        foreach (string ps in kvp.Value)
                        {
                            Console.WriteLine("\t->" + ps);
                        }
                    }
                }
            }
            #endregion

            Console.WriteLine("Analysing " + assemblyNamesAndPaths.Count + " assemblies.\n");

            IEnumerable<string> missingAssemblies = null;  
            IEnumerable<string> assemblyDirectories = null;

            bool hasRequiredAssemblies = AssemblyUtilities.RequiredAssembliesFound(assemblyNamesAndPaths.Values, dependencyDirs, out missingAssemblies, out assemblyDirectories);

            if (!hasRequiredAssemblies)
            {
                Console.WriteLine("Not all the required assemblies could be found. Analysis cannot continue.");
                Console.WriteLine();

                Console.WriteLine("The following assemblies are missing: ");
                foreach (string ma in missingAssemblies)
                {
                    Console.WriteLine("\t" + ma);
                }

                Console.WriteLine();

                Console.WriteLine("As well as the GAC and executing assembly directory, the following directories were probed for matching assemblies: ");
                foreach (string ad in assemblyDirectories)
                {
                    Console.WriteLine("\t" + ad);
                }
            }
            else
            {
                // load assemblies and run
                TypeMetadataGraphBuilder graphBuilder = new TypeMetadataGraphBuilder();
                graphBuilder.Verbose = verbose;

                Stopwatch sw = new Stopwatch();

                Console.WriteLine("Analysing the following assemblies:");
                foreach(string s in assemblyNamesAndPaths.Values)
                {
                    Console.WriteLine(s);
                }
                Console.WriteLine();

                sw.Start();
                graphBuilder.AnalyseAssemblies(assemblyNamesAndPaths, assemblyDirectories);

                graphBuilder.AssemblyGraph.Graph2XML(outputFileName, outputPath);
                sw.Stop();

                if (graphBuilder.Errors.Count > 0)
                {
                    string errorFileName = "errors-in-" + outputFileName + ".txt";
                    using (TextWriter tw = new StreamWriter(Path.Combine(outputPath, errorFileName)))
                    {
                        foreach (string error in graphBuilder.Errors)
                        {
                            tw.WriteLine(error);
                        }
                    }
                }

                Console.WriteLine("Completed analysis in {0} with {1} errors.", sw.Elapsed, graphBuilder.Errors.Count);
            }

            if (pauseAtCompletion)
            {
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey(true);
            }
        }

        private static void ShowHelp(OptionSet optionSet)
        {
            Console.WriteLine("CILADA extracts the type information from within a set of assemblies.");
            Console.WriteLine();
            Console.WriteLine("Usage: cilada [OPTIONS] fullassemblypaths");
            Console.WriteLine(@"Example Usage: cilada -o 'C:\cilada\results' -n 'AnalysisResult' -d 'C:\Program Files\MyApp' -r 'C:\cilada\assemblyone.dll' 'C:\cilada\assemblytwo.dll' -p");
            Console.WriteLine();
            Console.WriteLine("Options:");
            optionSet.WriteOptionDescriptions(Console.Out);
            Console.WriteLine();
            Console.WriteLine("\tcilada " + Assembly.GetExecutingAssembly().GetName().Version);

            Console.WriteLine("Press any key to quit...");
            Console.ReadKey(true);
        }

        private static List<String> ProcessPaths(List<string> paths, bool recursive)
        {
            // validate each path in paths
            HashSet<string> validPaths = new HashSet<string>();

            foreach (string path in paths)
            {
                if (Directory.Exists(path))
                {
                    DirectoryInfo dir = new DirectoryInfo(path);
                    List<string> newPaths = ProcessDirectory(dir, recursive);
                    validPaths.UnionWith(newPaths);
                }
                else if (File.Exists(path))
                {
                    if (IsAssembly(new FileInfo(path)))
                    {
                        validPaths.Add(path);
                    }
                }
                else
                {
                    Console.WriteLine("{0} is an invalid path", path);
                }
            }

            return validPaths.ToList();
        }

        /// <summary>
        /// Verifies that the given path is an assembly. Does not check that it is a .net assembly.
        /// This assumes that the file exists.
        /// </summary>
        /// <param name="path">The path to check</param>
        private static bool IsAssembly(FileInfo path)
        {
            if (ASSEMBLY_EXTENSIONS.Contains(path.Extension.ToLower()))
            {
                // need a check that it is a .net assembly
                return true;
            }

            return false;
        }

        private static List<string> GetAllSubDirs(DirectoryInfo dir)
        {
            List<string> paths = new List<string>();

            foreach (DirectoryInfo d in dir.GetDirectories())
            {
                paths.Add(d.FullName);
                List<string> subDirAssemblies = GetAllSubDirs(d);
                paths.AddRange(subDirAssemblies);
            }

            return paths;
        }

        private static List<string> ProcessDirectory(DirectoryInfo dir, bool recursive)
        {
            HashSet<string> assemblies = new HashSet<string>();

            // process files
            foreach (FileInfo f in dir.GetFiles())
            {
                if (IsAssembly(f))
                {
                    assemblies.Add(f.FullName);
                }
            }

            if (recursive)
            {
                foreach (DirectoryInfo d in dir.GetDirectories())
                {
                    List<string> subDirAssemblies = ProcessDirectory(d, recursive);
                    assemblies.UnionWith(subDirAssemblies);
                }
            }

            return assemblies.ToList();
        }
    }
}